﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;

namespace Chat.Server
{
    /// <summary>
    /// For each client command, received by the ServerEndPoint instanse
    /// (see ServerEndPoint class below) there is a personal event, risen upon
    /// this specific command received.
    /// This class is the base class for _all_ event arguments classes, each
    /// for a specific command received from the client.
    /// </summary>
    /// <typeparam name="Cmd">class inherited from Client.ClientCmd</typeparam>
    public abstract class ClientCmdEventArgs<Cmd> : CmdEventArgs<Cmd>
        where Cmd : Client.ClientCmd
    {
        public ClientCmdEventArgs(Cmd commandReceived) :
            base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Args for event risen upon Client.MessageCmd command received.
    /// </summary>
    public class MessageEventArgs : ClientCmdEventArgs<Client.MessageCmd>
    {
        public MessageEventArgs(Client.MessageCmd commandReceived)
            : base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Args for event risen upon Client.LogoutCmd received.
    /// </summary>
    public class LogoutEventArgs : ClientCmdEventArgs<Client.LogoutCmd>
    {
        public LogoutEventArgs(Client.LogoutCmd commandReceived)
            : base(commandReceived)
        {
        }
    }
    /// <summary>
    /// Exception thrown by the ServerEndPoint instanse when the chat
    /// client doesn't send a Client.LoginCmd command within some
    /// time interval after the connection establishment.
    /// </summary>
    public class LoginTimeoutException : ApplicationException
    {
        public LoginTimeoutException(Exception innerException)
            : base("Client login timeout", innerException)
        {
        }
    }

    /// <summary>
    /// Implementation of the BaseEndPoint class on the server side.
    /// MUST SEE the BaseEndPoint class documentation before.
    /// </summary>
    public class ServerEndPoint : BaseEndPoint<ServerCmd>
    {
        /// <summary>
        /// Initializes the new instanse of the ServerEndPoint class with the
        /// specified communication channel between the chat server
        /// and a particular chat client and with the specified serializer
        /// that will be used by the server to communicate with the client
        /// over the communication channel.
        /// </summary>
        /// <param name="connection">
        /// The communication channel between the server and the particular client.
        /// </param>
        /// <param name="serializer">
        /// The serializer that will be used when communicating with the client
        /// over the communication channel.
        /// </param>
        public ServerEndPoint(
            Connection<ServerCmd> connection,
            CmdSerializer<ServerCmd> serializer) :
            base(connection, serializer)
        {
            //
            // The timeout to wait for the client to send the login command.
            Connection.ReceiveTimeout = 1000;

            object firstCommand;

            try
            {
                firstCommand = ReceiveCommand();
            }
            catch (Exception ex)
            {
                try
                {
                    SendCommand(
                        new LoginFailedCmd("Login timeout"),
                        SendCommandMode.SendOnly);
                }
                catch (Exception)
                {
                }
                
                throw ex;
            }
            finally
            {
                Connection.ReceiveTimeout = 0;
            }

            Client.LoginCmd loginCommand = firstCommand as Client.LoginCmd;

            //
            // Check if the first command received from the client is indeed the
            // login command.
            if (loginCommand == null)
            {
                UnknownCmdException ex = new UnknownCmdException(firstCommand);

                try
                {
                    SendCommand(
                        new LoginFailedCmd(ex.Message), SendCommandMode.SendOnly);
                }
                catch (Exception)
                {
                }

                throw new UnknownCmdException(firstCommand);
            }

            LoginData = loginCommand.LoginData;
        }

        /// <summary>
        /// The static wrapper for the ServerEndPoint class constructor.
        /// Will close the specified connection upon any exception thrown
        /// from the constructor.
        /// </summary>
        /// <param name="connection">
        /// The communication channel between the server and the particular client.
        /// </param>
        /// <param name="serializer">
        /// The serializer that will be used when communicating with the client
        /// over the communication channel.
        /// </param>
        /// <returns>The instanse of the ServerEndPoint class.</returns>
        public static ServerEndPoint Create(
            Connection<ServerCmd> connection, CmdSerializer<ServerCmd> serializer)
        {
            try
            {
                ServerEndPoint sep = new ServerEndPoint(
                    connection, serializer);
                return sep;
            }
            catch (Exception ex)
            {
                connection.Close();
                throw ex;
            }
        }

        /// <summary>
        /// Occurs when chat text message received from the client,
        /// associated with this ServerEndPoint instanse.
        /// </summary>
        public event EventHandler<MessageEventArgs> MessageReceived;
        /// <summary>
        /// Occurs when logout command received from the client,
        /// associated with this ServerEndPoint instanse.
        /// </summary>
        public event EventHandler<LogoutEventArgs> Logout;

        /// <summary>
        /// Sends the chat text message notification to the client,
        /// associated with this ServerEndPoint instanse. The sending is done
        /// in the specified SendCommandMode mode.
        /// </summary>
        /// <param name="to">The recepient of the original message sent.</param>
        /// <param name="message">The message text.</param>
        /// <param name="sender">User nick name who sent the original message.</param>
        /// <param name="sendMode">In which mode the sending should be done.</param>
        public void SendMessage(
            NickName to, string message, LoginData sender, SendCommandMode sendMode)
        {
            MessageCmd messageCmd = new MessageCmd(
                new MessageData(to, message, sender.User), sender.TextColor);

            SendCommand(messageCmd, sendMode);
        }
        /// <summary>
        /// Exactly as the overloaded SendMessage with the specified sending mode.
        /// This method sends in DisposeThrow mode.
        /// </summary>
        public void SendMessage(NickName to, string message, LoginData sender)
        {
            SendMessage(to, message, sender, SendCommandMode.DisposeThrow);
        }

        /// <summary>
        /// Locking object used by the SendMessageWithLock method.
        /// </summary>
        private readonly object sendMessageLock = new object();
        /// <summary>
        /// Exactly as the SendMessage method with the same signature, but the sending
        /// is done in a critical section.
        /// </summary>
        public void SendMessageWithLock(NickName to, string message, LoginData sender)
        {
            lock (sendMessageLock)
            {
                SendMessage(to, message, sender);
            }
        }

        /// <summary>
        /// Sends the list of currenly online users to the client,
        /// associated with this ServerEndPoint instanse. The sending is done
        /// in the specified SendCommandMode mode.
        /// </summary>
        /// <param name="users">The collection of users currently online.</param>
        /// <param name="sendMode">In which mode the sending should be done.</param>
        public void SendUsersOnline(ICollection<NickName> users, SendCommandMode sendMode)
        {
            string usersCsv = users.Count > 0 ? BuildCsv(users) : string.Empty;
            UsersOnlineCmd usrsOnlineCmd = new UsersOnlineCmd(usersCsv);

            SendCommand(usrsOnlineCmd, sendMode);
        }

        /// <summary>
        /// Exactly as the overloaded SendUsersOnline with the specified sending mode.
        /// This method sends in DisposeThrow mode.
        /// </summary>
        public void SendUsersOnline(ICollection<NickName> users)
        {
            SendUsersOnline(users, SendCommandMode.DisposeThrow);                
        }

        /// <summary>
        /// Sends notification about connection state change of the specified
        /// user to the client, associated with this ServerEndPoint instanse.
        /// The sending is done in the specified SendCommandMode mode.
        /// </summary>
        /// <param name="user">The user whose connection state change.</param>
        /// <param name="connectState">Whether the specified user connected or disconnected.</param>
        /// <param name="sendMode">In which mode the sending should be done.</param>
        public void SendUserUpdate(
            NickName user, UserConnectState connectState, SendCommandMode sendMode)
        {
            UsersUpdateCmd usersUpdateCmd = new UsersUpdateCmd(
                connectState, user.Value);
            SendCommand(usersUpdateCmd, sendMode);
        }

        /// <summary>
        /// Exactly as the overloaded SendUserUpdate with the specified sending mode.
        /// This method sends in DisposeThrow mode.
        /// </summary>
        public void SendUserUpdate(NickName user, UserConnectState connectState)
        {
            SendUserUpdate(user, connectState, SendCommandMode.DisposeThrow);
        }

        /// <summary>
        /// Sends notification about connection state change of the specified
        /// users to the client, associated with this ServerEndPoint instanse.
        /// The sending is done in the specified SendCommandMode mode.
        /// </summary>
        /// <param name="users">The user whose connection state change.</param>
        /// <param name="connectState">Whether the specified user connected or disconnected.</param>
        /// <param name="sendMode">In which mode the sending should be done.</param>
        public void SendUsersUpdate(
            ICollection<NickName> users,
            UserConnectState connectState,
            SendCommandMode sendMode)
        {
            string usersCsv = BuildCsv(users);
            UsersUpdateCmd usersUpdateCmd = new UsersUpdateCmd(connectState, usersCsv);

            SendCommand(usersUpdateCmd, sendMode);
        }

        public void SendUsersUpdate(
            ICollection<NickName> users, UserConnectState connectState)
        {
            SendUsersUpdate(users, connectState, SendCommandMode.DisposeThrow);
        }

        public void SendLoginOk()
        {
            SendLoginOk(SendCommandMode.DisposeThrow);
        }

        public void SendLoginOk(SendCommandMode sendMode)
        {
            LoginOkCmd loginOkCmd = new LoginOkCmd();

            SendCommand(loginOkCmd, sendMode);
        }

        public void SendLoginFailed(string reason)
        {
            SendLoginFailed(reason, SendCommandMode.DisposeThrow);
        }

        public void SendLoginFailed(string reason, SendCommandMode sendMode)
        {
            LoginFailedCmd loginFailedCmd = new LoginFailedCmd(reason);

            SendCommand(loginFailedCmd, sendMode);
        }

        /// <summary>
        /// Processes the chat command received from the client, associated with this
        /// ServerEndPoint instanse. For each valid command type raises event, specific
        /// for this type of client command.
        /// </summary>
        /// <param name="sender">The command receiver thread.</param>
        /// <param name="e">Event arguments containing the client command.</param>
        /// <returns>true, if the specified command is valid, false otherwise.</returns>
        protected override bool ProcessReceivedCommand(object sender, CmdReceivedEventArgs e)
        {
            if (e.CommandReceived is Client.MessageCmd)
                OnMessageReceived(new MessageEventArgs(
                    e.CommandReceived as Client.MessageCmd));
            else if (e.CommandReceived is Client.LogoutCmd)
            {
                //
                // Notifies the command receiver thread, where the command came from,
                // to terminate.
                e.Terminate = true;

                //
                // Releases the aquired resources.
                Dispose();

                OnLogout(new LogoutEventArgs(e.CommandReceived as Client.LogoutCmd));
            }
            else
                return false; // unknown command

            return true;
        }

        protected virtual void OnMessageReceived(MessageEventArgs e)
        {
            if (MessageReceived != null)
                MessageReceived(this, e);
        }

        protected virtual void OnLogout(LogoutEventArgs e)
        {
            if (Logout != null)
                Logout(this, e);
        }

        /// <summary>
        /// Builds the csv list of nicknames.
        /// </summary>
        private static string BuildCsv(ICollection<NickName> nickNames)
        {
            StringBuilder bldr = new StringBuilder();

            nickNames.Aggregate(
                bldr,
                (StringBuilder b, NickName nn) => b.Append(string.Format("{0},", nn.Value)));

            return bldr.Remove(bldr.Length - 1, 1).ToString();
        }
    }
}
